﻿using System;
using System.Collections.Generic;

namespace ObjectPoolSandbox
{
    public class ObjectPool
    {
        private static readonly object SyncLock = new object();

        private static Queue<PooledObject> m_Available;

        // could be a configuration value.
        public const int DefaultCapacity = 10;

        private const int GrowthAmount = DefaultCapacity / 5;

        /// <summary>
        /// Static constructor.
        /// Configures the settings for the class.
        /// </summary>
        static ObjectPool()
        {
            SetCapacity(DefaultCapacity);
        }

        /// <summary>
        /// Private setter as the pool needs to be reconstructed when the capacity changes, not a trivial operation.
        /// </summary>
        public static int Capacity { get; private set; }

        /// <summary>
        /// Returns the number of PooledObject instances currently available from the pool.
        /// </summary>
        public static int AvailableCount
        {
            get { return m_Available.Count; }
        }

        /// <summary>
        /// Get or sets a flag that indicates whether or not auto-expansion semantics are turned on for the ObjectPool.
        /// </summary>
        public static bool AutoExpand { get; set; }

        /// <summary>
        /// Retrieves an initialized instance from the ObjectPool.
        /// </summary>
        /// <returns>An initialized instance of the <see cref="PooledObject"/> class from the pool.</returns>
        /// <exception cref="OutOfResourcesException">If auto-expand semantics are disabled and all instances have been acquired this exception will be thrown.</exception>
        public static PooledObject Acquire()
        {
            lock (SyncLock)
            {
                if (m_Available.Count == 0)
                {
                    throw new OutOfResourcesException();
                }

                if (m_Available.Count == 1 && AutoExpand)
                {
                    ExpandPool();
                }
            
                return m_Available.Dequeue();
            }
        }

        /// <summary>
        /// Wrapper for calling Aquire/Release around a given method implementation. Relieves clients from having to be concerned with Release'ing Acquired instances back into the pool.
        /// </summary>
        /// <param name="action">A method group or lambda expression that takes a instance of the <see cref="ObjectPool"/> class and returns void. The method is executed with an instance obtained from the pool. The instance is added back into the pool at the completion of the provided method.</param>
        public static void LendObject(Action<PooledObject> action)
        {
            PooledObject o = null;
            try
            {
                o = Acquire();
                action(o);
            }
            finally
            {
                if (o != null)
                {
                    Release(o);
                }
            }
        }

        /// <summary>
        /// Returns the given instance to the pool.
        /// </summary>
        /// <param name="instance"></param>
        public static void Release(PooledObject instance)
        {
            m_Available.Enqueue(instance);
        }

        /// <summary>
        /// Resets the ObjectPool back to default settings.
        /// </summary>
        public static void Reset()
        {
            SetCapacity(DefaultCapacity);
            AutoExpand = false;
        }

        /// <summary>
        /// Changes the capcity of the ObjectPool to the provided value. This completely regenerates the instances within the pool and could be costly.
        /// </summary>
        /// <param name="capacity">int values specifying the new capacity for the ObectPool.</param>
        public static void SetCapacity(int capacity)
        {
            Capacity = capacity;
            BuildPool();
        }

        /// <summary>
        /// Using the Capacity creates the list and populates it with initialized instances of the <see cref="PooledObject"/> class.
        /// </summary>
        private static void BuildPool()
        {
            m_Available = new Queue<PooledObject>(Capacity);

            for (int index = 0; index < Capacity; index++)
            {
                m_Available.Enqueue(new PooledObject());
            }
        }

        /// <summary>
        /// Increases the number of items in the pool by the <see cref="GrowthAmount"/> value.
        /// </summary>
        /// <remarks>This method works independently of the Capacity value. There is currently no defined upper bound for the number of of instances in the pool.</remarks>
        private static void ExpandPool()
        {
            for (int index = 0; index < GrowthAmount; index++)
            {
                m_Available.Enqueue(new PooledObject());
            }
        }
    }
}